import { dom, EventHandler, Nodes } from '@knno/dom';
import { Grid, GridData } from '../grid/grid';
import { Column, GridEventHandler, GridEventMap, RowType } from '../grid/gridBase';
import { listStyle } from './list.css';

import SVGArrowRight from '@material-design-icons/svg/filled/keyboard_arrow_right.svg';
import SVGArrowDown from '@material-design-icons/svg/filled/keyboard_arrow_down.svg';
import { checkbox } from '../checkbox/checkbox';
import { DATA } from '../symbol';

export interface ColumnOption {
	/**
	 * if allow children then must both specify expand key and children key
	 */
	children?: number | string;
	/**
	 * if allow children then must both specify expand key and children key
	 */
	expand?: number | string;
	text?: number | string;
	value?: number | string;
	nonSelectableField?: number | string;
	formatter?: (row: RowType, cell: unknown, rowIndex: number) => Element | Text | (Element | Text)[];
}

const LEVEL_MARGIN = 1.0;

interface ListRowExpandEventDetail {
	rowIndex: number;
	row: unknown;
	expand: boolean;
}

type ListRowExpandEvent = CustomEvent<ListRowExpandEventDetail>;

export interface ListEventMap extends GridEventMap {
	rowexpand: ListRowExpandEvent;
	rowcheck: CustomEvent<{
		rowIndex: number;
		row: unknown;
		checked: boolean;
	}>;
}

export type ListEventKey = keyof ListEventMap;
export type ListEventHandler<T extends ListEventKey, O extends Nodes> = (this: O, event: ListEventMap[T]) => void;

interface ListRow {
	row: unknown;
	level: number;
	parent?: ListRow;
}

export type SelectType = 'any' | 'leaf' | 'cascade' | 'cascade-leaf';

export interface List {
	on<K extends keyof ListEventMap>(
		event: K,
		listener: ListEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	on<K extends keyof GridEventMap>(
		event: K,
		listener: GridEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	on<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;

	off<K extends keyof ListEventMap>(
		event: K,
		listener: ListEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof GridEventMap>(
		event: K,
		listener: GridEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
}

function getCell(row: RowType, col: ColumnOption): unknown {
	if (col.text === undefined || row === undefined || row === null || typeof row !== 'object') {
		return row;
	} else {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		return (row as any)[col.text];
	}
}

export function getValue(row: RowType, col: ColumnOption): unknown {
	const valueKey = col.value ?? col.text;
	if (valueKey === undefined || row === undefined || row === null || typeof row !== 'object') {
		return row;
	} else {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		return (row as any)[valueKey];
	}
}

export function getText(row: RowType, col: ColumnOption): string {
	const textKey = col.text ?? col.value;
	if (textKey === undefined || row === undefined || row === null || typeof row !== 'object') {
		return row + '';
	} else {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		return (row as any)[textKey];
	}
}

export function getChildren(row: RowType, col: ColumnOption): unknown[] | null {
	if (col.children === undefined) {
		return null;
	} else {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const children = (row as any)[col.children];
		if (children instanceof Array) {
			return children;
		} else {
			return null;
		}
	}
}

export interface List {
	[DATA]: GridData & { opt: ColumnOption; seltype: SelectType };
}

export class List extends Grid {
	constructor() {
		super();
		this[DATA].opt = {};
		this[DATA].seltype = 'any';
		this[DATA].config.borderBottom = 0;
		this.addClass(listStyle)
			.headLine(false)
			// .activable(true)
			.columnSize('scale');
		this.on('keydown', (evt) => {
			if (evt.key === 'ArrowLeft') {
				if (!this.expandActive(false)) {
					const row = super.activeRow();
					let parent: ListRow | undefined;
					if (row && (parent = (row as ListRow).parent)) {
						const idx = this.getSortedData().indexOf(parent);
						super.setActiveRow(idx, true);
					}
				}
				evt.preventDefault();
				evt.stopPropagation();
			} else if (evt.key === 'ArrowRight') {
				this.expandActive(true);
				evt.preventDefault();
				evt.stopPropagation();
			}
		});
	}

	private expandActive(expand: boolean): boolean {
		const idx = this.active();
		if (this.activable()) {
			const r = this.activeRow();
			if (r !== null && idx !== null && this.getChildren(r) != null) {
				const cur = this.isExpand(r);
				if (cur !== expand) {
					this.setExpand(r, expand);
					const evt = new CustomEvent<ListRowExpandEventDetail>('rowexpand', {
						detail: {
							row: r,
							rowIndex: idx,
							expand,
						},
					});
					this.emit(evt);
					this.refresh('data');
					return true;
				}
			}
		}
		return false;
	}

	private getRowCheckedState(row: RowType): boolean | 'half' {
		const children = this.getChildren(row);
		if (children === null) {
			return this[DATA].selected.has(row);
		} else {
			const checkedCount = children.filter((r) => this.getRowCheckedState(r) === true).length;
			if (children.length === 0 || checkedCount === 0) {
				return false;
			} else if (checkedCount === children.length) {
				return true;
			} else {
				return 'half';
			}
		}
	}

	private setRowCheckedState(row: RowType, checked: boolean, selectType: 'cascade' | 'cascade-leaf') {
		const children = this.getChildren(row);
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const selectable = !(this[DATA].opt.nonSelectableField ? (row as any)[this[DATA].opt.nonSelectableField] : false);
		if (children === null) {
			if (checked) {
				selectable && this[DATA].selected.add(row);
			} else {
				this[DATA].selected.delete(row);
			}
		} else {
			if (selectType === 'cascade') {
				if (checked) {
					selectable && this[DATA].selected.add(row);
				} else {
					this[DATA].selected.delete(row);
				}
			}
			children.forEach((r) => this.setRowCheckedState(r, checked, selectType));
		}
	}

	protected override getColumns(): Column[] {
		return [
			{
				width: '100%',
				formatter: (row, _, rowIdx): Element | Text | (Element | Text)[] => {
					const arrow = dom.span('arrow');
					const level = (row as ListRow).level;
					const r = (row as ListRow).row;
					arrow.css({
						marginLeft: level * LEVEL_MARGIN + 'em',
					});
					if (this.getChildren(r)) {
						if (this.isExpand(r)) {
							arrow.html(SVGArrowDown);
							arrow.on('mousedown', (ev) => {
								ev.preventDefault();
								(this[0] as HTMLElement).focus();
								this.setExpand(r, false);
								this.refresh('data');
								const evt = new CustomEvent<ListRowExpandEventDetail>('rowexpand', {
									detail: {
										row: r,
										rowIndex: rowIdx,
										expand: false,
									},
								});
								this.emit(evt);
							});
						} else {
							arrow.html(SVGArrowRight);
							arrow.on('mousedown', (ev) => {
								ev.preventDefault();
								(this[0] as HTMLElement).focus();
								this.setExpand(r, true);
								this.refresh('data');
								const evt = new CustomEvent<ListRowExpandEventDetail>('rowexpand', {
									detail: {
										row: r,
										rowIndex: rowIdx,
										expand: true,
									},
								});
								this.emit(evt);
							});
						}
					}
					const text = getCell(r, this[DATA].opt);
					const cell = dom.span('content');
					if (typeof this[DATA].opt.formatter === 'function') {
						cell.append(this[DATA].opt.formatter(r, text, rowIdx));
					} else {
						cell.append(dom.text(text + ''));
					}
					const chk: (Element | Text)[] = [];
					const selType = this.selectType();
					const selectable = !(this[DATA].opt.nonSelectableField
						? (r as any)[this[DATA].opt.nonSelectableField]
						: false);
					if (this.selectable() && !(selType === 'leaf' && this.getChildren(r)) && selectable) {
						const ck = checkbox().checked(
							selType === 'cascade' || selType === 'cascade-leaf'
								? this.getRowCheckedState(r)
								: this[DATA].selected.has(r)
						);
						ck.on('click', () => {
							const selType = this.selectType();
							if (selType === 'cascade' || selType === 'cascade-leaf') {
								this.setRowCheckedState(r, ck.checked() === true, selType);
								this.refresh('data');
							} else {
								if (ck.checked()) {
									this[DATA].selected.add(r);
								} else {
									this[DATA].selected.delete(r);
								}
							}
							this.emit(new CustomEvent('rowcheck', { detail: { row: r, rowIndex: rowIdx, checked: ck.checked() } }));
						});
						ck.on('mousedown', (evt) => {
							evt.preventDefault();
							(this[0] as HTMLElement).focus();
							this.active(rowIdx);
							ck.emit(new MouseEvent('click'));
						});
						chk.push(...ck);
					}
					return [...arrow, ...chk, ...cell];
				},
			},
		];
	}

	private getChildren(row: unknown): unknown[] | null {
		let children: unknown[];
		if (
			this[DATA].opt.children !== undefined &&
			(children = (row as never)[this[DATA].opt.children]) &&
			children instanceof Array
		) {
			return children;
		} else {
			return null;
		}
	}

	private isExpand(row: unknown): boolean {
		return this[DATA].opt.expand !== undefined && !!(row as never)[this[DATA].opt.expand];
	}

	private setExpand(row: unknown, expand: boolean) {
		if (this[DATA].opt.expand !== undefined) {
			(row as Record<string | number, boolean>)[this[DATA].opt.expand] = expand;
		}
	}

	protected override isRowDisable(row: unknown): boolean {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		return this[DATA].disableField !== null ? !!((row as ListRow).row as any)?.[this[DATA].disableField] : false;
	}

	protected override getSortedData(): ListRow[] {
		if (this[DATA].sorted) {
			return this[DATA].sorted as ListRow[];
		}
		const addRow = (result: ListRow[], row: unknown, level: number, parent?: ListRow) => {
			const children = this.getChildren(row);
			if (children && this.isExpand(row)) {
				const listRow: ListRow = { row, level, parent };
				result.push(listRow);
				for (let i = 0; i < children.length; i++) {
					addRow(result, children[i], level + 1, listRow);
				}
			} else {
				result.push({ row, level, parent });
			}
		};
		const result: ListRow[] = [];
		for (let i = 0; i < this[DATA].data.length; i++) {
			addRow(result, this[DATA].data[i], 0);
		}
		this[DATA].sorted = result;
		return result;
	}

	column(): ColumnOption;
	column(opt: ColumnOption): this;
	column(opt?: ColumnOption): this | ColumnOption {
		if (opt === undefined) {
			return this[DATA].opt;
		}
		this[DATA].opt = opt;
		this.refresh('complete');
		return this;
	}

	override columns(): Column[];
	override columns(cols: Column[]): this;
	override columns(cols?: Column[]): this | Column[] {
		if (cols === undefined) {
			return super.columns();
		}
		return this;
	}

	override activeRow(): RowType | null {
		const r = super.activeRow();
		if (r === null) return null;
		return (r as ListRow).row;
	}

	selectType(): SelectType;
	selectType(opt: SelectType): this;
	selectType(opt?: SelectType): SelectType | this {
		if (opt === undefined) {
			return this[DATA].seltype;
		}
		this[DATA].selected.clear();
		this[DATA].seltype = opt;
		this.refresh('data');
		return this;
	}

	override selected(): RowType[];
	override selected(rows: RowType[]): this;
	override selected(rows?: RowType[]): this | RowType[] {
		if (rows === undefined) {
			return [...this[DATA].selected];
		}
		const cols = this.column();
		const values = new Set(rows.map((value) => getValue(value, cols)));
		const find = (arr: unknown[]): unknown[] => {
			return arr.flatMap((r) => {
				const children = getChildren(r, cols);
				if (children) return [r, ...find(children)];
				else return [r];
			});
		};
		const checked = find(this[DATA].data).filter((row) => {
			return values.has(getValue(row, cols));
		});
		this[DATA].selected = new Set(checked);
		this.renderHead();
		this.genStyle();
		this.measure('selection');
		this.renderBody();
		return this;
	}

	activeText(): unknown | null {
		const activeRow = this.activeRow();
		if (activeRow === null) return null;
		return getCell(activeRow, this[DATA].opt);
	}

	activeValue(): unknown | null;
	activeValue(value: unknown, focus: boolean): this;
	activeValue(value?: unknown, focus = false): this | unknown | null {
		if (value === undefined) {
			const activeRow = this.activeRow();
			if (activeRow === null) return null;
			return getValue(activeRow, this[DATA].opt);
		}
		const expandKey = this[DATA].opt.expand;
		const find = (arr: unknown[]): boolean => {
			for (let i = 0; i < arr.length; i++) {
				const row = arr[i];
				if (getValue(row, this[DATA].opt) === value) {
					return true;
				} else if (expandKey !== undefined && typeof row === 'object' && row !== null) {
					const children = getChildren(row, this[DATA].opt);
					if (children && find(children)) {
						(row as Record<string | number, boolean>)[expandKey] = true;
						return true;
					}
				}
			}
			return false;
		};
		if (find(this[DATA].data)) {
			this.refresh('data');
			const idx = this.getSortedData().findIndex((row) => getValue(row.row, this[DATA].opt) === value);
			this.active(idx, focus);
		}
		return this;
	}
}

export function list(...className: string[]): List {
	return new List().addClass(...className);
}
